package org.springframework.cloud.openfeign;

import me.spring.cloud.common.components.util.enums.SepartorTypeEnum;
import me.spring.cloud.common.components.util.lang.StringUtil;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.ClassMetadata;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.AbstractClassTestingTypeFilter;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;

/**
 * @author luffy
 * @description: feign client 注册
 * @date 2019-05-13 17:07
 */
class QKTXFeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {

  // patterned after Spring Integration IntegrationComponentScanRegistrar
  // and RibbonClientsConfigurationRegistgrar

  private ResourceLoader resourceLoader;

  private Environment environment;

  public QKTXFeignClientsRegistrar() {
  }

  @Override
  public void setResourceLoader(ResourceLoader resourceLoader) {
    this.resourceLoader = resourceLoader;
  }

  @Override
  public void registerBeanDefinitions(AnnotationMetadata metadata,
      BeanDefinitionRegistry registry) {
    registerDefaultConfiguration(metadata, registry);
    registerFeignClients(metadata, registry);
  }

  private void registerDefaultConfiguration(AnnotationMetadata metadata,
      BeanDefinitionRegistry registry) {
    Map<String, Object> defaultAttrs = metadata
        .getAnnotationAttributes(EnableQKTXFeignClient.class.getName(), true);

    if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
      String name;
      if (metadata.hasEnclosingClass()) {
        name = "default." + metadata.getEnclosingClassName();
      } else {
        name = "default." + metadata.getClassName();
      }
      registerClientConfiguration(registry, name,
          defaultAttrs.get("defaultConfiguration"));
    }
  }

  public void registerFeignClients(AnnotationMetadata metadata,
      BeanDefinitionRegistry registry) {
    ClassPathScanningCandidateComponentProvider scanner = getScanner();

    MultiValueMap<String, Object> m_attrs = metadata
        .getAllAnnotationAttributes(EnableQKTXFeignClient.class.getName());
    scanner.setResourceLoader(this.resourceLoader);
    final Class<?>[] clients;
    if (null != m_attrs) {
      List<Object> m_clients = m_attrs.get("clients");
      if (null != m_clients) {
        List<Class<?>> list = new ArrayList<>();
        m_clients.stream().map(o -> Arrays.asList((Class<?>[]) o)).forEach(list::addAll);
        clients = new Class[list.size()];
        list.toArray(clients);
      } else {
        clients = null;
      }
    } else {
      clients = null;
    }
    AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(FeignClient.class);
    Set<String> basePackages;
    if (clients == null || clients.length == 0) {
      scanner.addIncludeFilter(annotationTypeFilter);
      basePackages = getBasePackages(metadata);
    } else {
      final Set<String> clientClasses = new HashSet<>();
      basePackages = new HashSet<>();
      for (Class<?> clazz : clients) {
        basePackages.add(ClassUtils.getPackageName(clazz));
        clientClasses.add(clazz.getCanonicalName());
      }
      AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
        @Override
        protected boolean match(ClassMetadata metadata) {
          String cleaned = metadata.getClassName().replaceAll("\\$", ".");
          return clientClasses.contains(cleaned);
        }
      };
      scanner.addIncludeFilter(
          new QKTXFeignClientsRegistrar.AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
    }

    for (String basePackage : basePackages) {
      Set<BeanDefinition> candidateComponents = scanner
          .findCandidateComponents(basePackage);
      for (BeanDefinition candidateComponent : candidateComponents) {
        if (candidateComponent instanceof AnnotatedBeanDefinition) {
          // verify annotated class is an interface
          AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
          AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
          Assert.isTrue(annotationMetadata.isInterface(),
              "@FeignClient can only be specified on an interface");

          Map<String, Object> attributes = annotationMetadata
              .getAnnotationAttributes(
                  FeignClient.class.getCanonicalName());

          String name = getClientName(attributes);
          registerClientConfiguration(registry, name,
              attributes.get("configuration"));

          registerFeignClient(registry, annotationMetadata, attributes);
        }
      }
    }
  }

  private void registerFeignClient(BeanDefinitionRegistry registry,
      AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
    String className = annotationMetadata.getClassName();
    BeanDefinitionBuilder definition = BeanDefinitionBuilder
        .genericBeanDefinition(QKTXFeignClientsRegistrar.class);
    validate(attributes);
    definition.addPropertyValue("url", getUrl(attributes));
    definition.addPropertyValue("path", getPath(attributes));
    String name = StringUtil.getLastSplit(className, SepartorTypeEnum.SP_POINT.getLeab()) + getName(attributes);
    definition.addPropertyValue("name", name);
    definition.addPropertyValue("type", className);
    definition.addPropertyValue("decode404", attributes.get("decode404"));
    definition.addPropertyValue("fallback", attributes.get("fallback"));
    definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
    definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);

    String alias = name + "FeignClient";
    AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();

    boolean primary = (Boolean) attributes.get("primary"); // has a default, won't be null

    beanDefinition.setPrimary(primary);

    String qualifier = getQualifier(attributes);
    if (StringUtils.hasText(qualifier)) {
      alias = qualifier;
    }

    BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
        new String[]{alias});
    BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
  }

  private void validate(Map<String, Object> attributes) {
    AnnotationAttributes annotation = AnnotationAttributes.fromMap(attributes);
    // This blows up if an aliased property is overspecified
    // FIXME annotation.getAliasedString("name", FeignClient.class, null);
    Assert.isTrue(
        !annotation.getClass("fallback").isInterface(),
        "Fallback class must implement the interface annotated by @FeignClient"
    );
    Assert.isTrue(
        !annotation.getClass("fallbackFactory").isInterface(),
        "Fallback factory must produce instances of fallback classes that implement the interface annotated by @FeignClient"
    );
  }

  /* for testing */ String getName(Map<String, Object> attributes) {
    String name = (String) attributes.get("serviceId");
    if (!StringUtils.hasText(name)) {
      name = (String) attributes.get("name");
    }
    if (!StringUtils.hasText(name)) {
      name = (String) attributes.get("value");
    }
    name = resolve(name);
    if (!StringUtils.hasText(name)) {
      return "";
    }

    String host = null;
    try {
      String url;
      if (!name.startsWith("http://") && !name.startsWith("https://")) {
        url = "http://" + name;
      } else {
        url = name;
      }
      host = new URI(url).getHost();

    } catch (URISyntaxException e) {
    }
    Assert.state(host != null, "Service id not legal hostname (" + name + ")");
    return name;
  }

  private String resolve(String value) {
    if (StringUtils.hasText(value)) {
      return this.environment.resolvePlaceholders(value);
    }
    return value;
  }

  private String getUrl(Map<String, Object> attributes) {
    String url = resolve((String) attributes.get("url"));
    if (StringUtils.hasText(url) && !(url.startsWith("#{") && url.contains("}"))) {
      if (!url.contains("://")) {
        url = "http://" + url;
      }
      try {
        new URL(url);
      } catch (MalformedURLException e) {
        throw new IllegalArgumentException(url + " is malformed", e);
      }
    }
    return url;
  }

  private String getPath(Map<String, Object> attributes) {
    String path = resolve((String) attributes.get("path"));
    if (StringUtils.hasText(path)) {
      path = path.trim();
      if (!path.startsWith("/")) {
        path = "/" + path;
      }
      if (path.endsWith("/")) {
        path = path.substring(0, path.length() - 1);
      }
    }
    return path;
  }

  protected ClassPathScanningCandidateComponentProvider getScanner() {
    return new ClassPathScanningCandidateComponentProvider(false, this.environment) {
      @Override
      protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
        boolean isCandidate = false;
        if (beanDefinition.getMetadata().isIndependent()) {
          if (!beanDefinition.getMetadata().isAnnotation()) {
            isCandidate = true;
          }
        }
        return isCandidate;
      }
    };
  }

  protected Set<String> getBasePackages(AnnotationMetadata importingClassMetadata) {
    MultiValueMap<String, Object> m_attributes = importingClassMetadata
        .getAllAnnotationAttributes(EnableQKTXFeignClient.class.getCanonicalName());
    Set<String> basePackages = new HashSet<>();

    m_attributes.get("value")
        .forEach(o -> Arrays.stream((String[]) o).filter(StringUtils::hasText).forEach(basePackages::add));

    m_attributes.get("basePackages")
        .forEach(o -> Arrays.stream((String[]) o).filter(StringUtils::hasText).forEach(basePackages::add));

    m_attributes.get("basePackageClasses")
        .forEach(o -> Arrays.stream((Class[]) o).map(ClassUtils::getPackageName).forEach(basePackages::add));

    if (basePackages.isEmpty()) {
      basePackages.add(
          ClassUtils.getPackageName(importingClassMetadata.getClassName()));
    }
    return basePackages;
  }

  private String getQualifier(Map<String, Object> client) {
    if (client == null) {
      return null;
    }
    String qualifier = (String) client.get("qualifier");
    if (StringUtils.hasText(qualifier)) {
      return qualifier;
    }
    return null;
  }

  private String getClientName(Map<String, Object> client) {
    if (client == null) {
      return null;
    }
    String value = (String) client.get("value");
    if (!StringUtils.hasText(value)) {
      value = (String) client.get("name");
    }
    if (!StringUtils.hasText(value)) {
      value = (String) client.get("serviceId");
    }
    if (StringUtils.hasText(value)) {
      return value;
    }

    throw new IllegalStateException("Either 'name' or 'value' must be provided in @"
        + FeignClient.class.getSimpleName());
  }

  private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
      Object configuration) {
    BeanDefinitionBuilder builder = BeanDefinitionBuilder
        .genericBeanDefinition(FeignClientSpecification.class);
    builder.addConstructorArgValue(name);
    builder.addConstructorArgValue(configuration);
    registry.registerBeanDefinition(
        name + "." + FeignClientSpecification.class.getSimpleName(),
        builder.getBeanDefinition());
  }

  @Override
  public void setEnvironment(Environment environment) {
    this.environment = environment;
  }

  /**
   * Helper class to create a {@link TypeFilter} that matches if all the delegates match.
   *
   * @author Oliver Gierke
   */
  private static class AllTypeFilter implements TypeFilter {

    private final List<TypeFilter> delegates;

    /**
     * Creates a new {@link QKTXFeignClientsRegistrar.AllTypeFilter} to match if all the given delegates match.
     *
     * @param delegates must not be {@literal null}.
     */
    public AllTypeFilter(List<TypeFilter> delegates) {
      Assert.notNull(delegates, "This argument is required, it must not be null");
      this.delegates = delegates;
    }

    @Override
    public boolean match(MetadataReader metadataReader,
        MetadataReaderFactory metadataReaderFactory) throws IOException {

      for (TypeFilter filter : this.delegates) {
        if (!filter.match(metadataReader, metadataReaderFactory)) {
          return false;
        }
      }

      return true;
    }
  }
}
